package utils.binary.io;

import org.jetbrains.annotations.NotNull;
import utils.exception.RuntimeIOException;

import java.io.IOException;
import java.io.OutputStream;

/**
 * <h1>字节输出缓冲区</h1>
 * <p>提供了一种避免字节数组在内存复制的缓冲写入的实现，是线程安全的</p>
 *
 * @author Zhou Yifan
 */
public class BytesOutputBuffer {

    private static final int DEFAULT_CAPACITY = 8;

    private volatile int size = 0;

    private int capacity = 0;

    private int cursor = 0;

    private byte[][] buffers;

    public int getSize() {
        return size;
    }

    public byte[][] getBuffers() {
        return buffers;
    }

    public BytesOutputBuffer() {
        this(DEFAULT_CAPACITY);
    }

    public BytesOutputBuffer(int initCapacity) {
        if (initCapacity < 0) {
            throw new IllegalArgumentException(
                    "Init capacity is negative! --[initCapacity="
                            + initCapacity + "]"
            );
        }
        this.capacity = initCapacity;
        buffers = new byte[initCapacity][];
    }

    /**
     * <p>直接写入</p>
     * <p>此方法直接引用参数指定的数组作为缓冲区的组成部分，调用者需要避免在执行之后对参数引用的字节数组再做任何更改</p>
     * <p>设计此方法的目的是提供一种可以避免字节数组赋值操作来提升写入性能的方法</p>
     *
     * @param data 待写入的 byte 数组
     */
    public synchronized void write(byte[] data) {
        if (data == null) {
            throw new IllegalArgumentException("data is null!");
        }
        int index = cursor;
        if (index == capacity) {
            growCapacity();
        }
        buffers[index] = data;
        cursor++;
        size += data.length;
    }

    /**
     * 扩容
     */
    private void growCapacity() {
        int newCapacity = capacity + capacity / 2;
        if (newCapacity == capacity) {
            newCapacity++;
        }
        byte[][] newBuffers = new byte[newCapacity][];
        System.arraycopy(buffers, 0, newBuffers, 0, capacity);
        buffers = newBuffers;
        capacity = newCapacity;
    }

    /**
     * 写入 {@link BytesOutputBuffer}
     *
     * @param buffer {@link BytesOutputBuffer}
     */
    public void write(@NotNull BytesOutputBuffer buffer) {
        byte[][] fromBuffers = buffer.buffers;
        for (int i = 0; i < buffer.cursor; i++) {
            write(fromBuffers[i]);
        }
    }

    /**
     * <p>复制写入</p>
     * <p>此方法复制参数指定的数组的一个副本作为写入缓冲区的组成部分</p>
     *
     * @param data 待写入的字节数组
     */
    public void writeCopy(byte @NotNull [] data) {
        int length = data.length;
        byte[] copy = new byte[length];
        System.arraycopy(data, 0, copy, 0, length);
        write(copy);
    }

    /**
     * <p>复制写入</p>
     * <p>此方法复制参数指定的数组的一个副本作为写入缓冲区的组成部分</p>
     *
     * @param data   待写入的字节数组
     * @param offset 起始位置
     * @param length 写入的长度
     */
    public void writeCopy(byte[] data, int offset, int length) {
        byte[] copy = new byte[length];
        System.arraycopy(data, offset, copy, 0, length);
        write(copy);
    }

    /**
     * <p>把结果输出到指定的缓冲区，并返回写入的长度</p>
     * <p>如果指定的缓冲区的空间足够，则将写入全部数据，返回值等于 {@link #getSize()}</p>
     * <p>如果指定的缓冲区的空间足够，则写满为止，返回实际写入的数据大小</p>
     * <p>调用者可以把返回值与 {@link #getSize()} 比较，以判断是否完成写入</p>
     *
     * @param outBuffer 指定缓存区
     * @param offset    指定位置
     * @return int 值
     */
    public synchronized int writeTo(byte @NotNull [] outBuffer, int offset) {
        int length = Math.min(size, outBuffer.length - offset);
        int t = length;
        int s;
        for (int i = 0; i < cursor && t > 0; i++) {
            s = Math.min(buffers[i].length, t);
            System.arraycopy(buffers[i], 0, outBuffer, offset, s);
            offset += s;
            t -= s;
        }
        return length;
    }

    public synchronized int writeTo(OutputStream outputStream) {
        try {
            for (int i = 0; i < cursor; i++) {
                outputStream.write(buffers[i]);
            }
            return size;
        } catch (IOException e) {
            throw new RuntimeIOException(e.getMessage(), e);
        }
    }

    /**
     * 返回所有内容的副本
     *
     * @return byte 数组
     */
    public byte[] toBytes() {
        byte[] data = new byte[size];
        writeTo(data, 0);
        return data;
    }
}
